/*
 * Copyright 2006 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.rebind.rpc;

import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.rpc.Serializer;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

/**
 * This class generates a class that is able to serialize and deserialize a set
 * of types into or out of a stream.
 */
public class TypeSerializerCreator {
  private SourceWriter srcWriter;
  private JClassType svcInterface;
  private SerializableTypeOracle serializationOracle;
  private JType[] serializableTypes;
  private JClassType stringType;
  private boolean enforceTypeVersioning;

  public TypeSerializerCreator(SerializableTypeOracle serializationOracle) {
    if (serializationOracle == null) {
      throw new NullPointerException("null serialization oracle");
    }

    this.serializationOracle = serializationOracle;
  }

  private void initializeProperties(TreeLogger logger,
      GeneratorContext context) {
    PropertyOracle propertyOracle = context.getPropertyOracle();
    try {
      String propVal = propertyOracle.getPropertyValue(logger,
        Shared.RPC_PROP_ENFORCE_TYPE_VERSIONING);
      if (propVal != null && propVal.equals("false")) {
        enforceTypeVersioning = false;
      } else {
        enforceTypeVersioning = true;
      }

      return;
    } catch (BadPropertyValueException e) {
      // Purposely ignored, because we want to enforce RPC versioning if
      // the property is not defined
      //
    }

    enforceTypeVersioning = true;
  }

  private boolean shouldEnforceTypeVersioning() {
    return enforceTypeVersioning;
  }

  public String realize(TreeLogger logger, GeneratorContext context,
      JClassType serviceInterface) throws IOException,
      UnableToCompleteException {
    TypeOracle typeOracle = context.getTypeOracle();
    assert (typeOracle != null);

    assert (serviceInterface != null);
    setServiceInterface(serviceInterface);

    initializeProperties(logger, context);

    logger = logger.branch(TreeLogger.SPAM,
      "Generating TypeSerializer for service interface '"
        + serviceInterface.getQualifiedSourceName() + "'", null);
    String generatedTypeNames = serializationOracle
      .getTypeSerializerQualifiedName(getServiceInterface());

    generatedTypeNames += ";" + createFieldSerializers(logger, context);

    try {
      srcWriter = getSourceWriter(logger, context);
      if (srcWriter == null) {
        return generatedTypeNames;
      }

      srcWriter.indent();

      writeInstantiationMethods(context);

      writeDeserializeMethod(context);

      writeDeserializeImplMethod(context);

      writeSerializeMethod();

      writeSerializeImplMethod(context);

      writeGetSerializationSignatureMethod(context);
      
      writeGetSerializationSignatureMethodImpl(context);

      writeRaiseSerializationException();

      srcWriter.outdent();

      srcWriter.commit(logger);

    } catch (ClassNotFoundException e) {
      logger.log(TreeLogger.ERROR, "Could not find class '" + e.getMessage()
        + "'", null);
      throw new UnableToCompleteException();

    }

    return generatedTypeNames;
  }

  private void writeRaiseSerializationException() {
    srcWriter
      .println("private static void raiseSerializationException(String msg)  throws "
        + Shared.SERIALIZATION_EXCEPTION_CLASS_NAME + " {");
    srcWriter
      .indentln("throw new com.google.gwt.user.client.rpc.SerializationException(msg);");
    srcWriter.println("}");
  }

  private JType[] initializeSerializableTypes(GeneratorContext ctx) {
    ArrayList list = new ArrayList();

    JType[] types = serializationOracle.getSerializableTypes();
    for (int index = 0; index < types.length; ++index) {
      JType type = types[index];

      // Ignore primitives
      if (type.isPrimitive() != null) {
        continue;
      }

      // Ignore interfaces
      if (type.isInterface() != null) {
        continue;
      }

      // Ignore parameterizedTypes
      if (type.isParameterized() != null) {
        continue;
      }
      
      list.add(type);
    }

    JType[] serTypes = (JType[]) list.toArray(new JType[list.size()]);

    // Sort the types by name so that generated code will be repeatable.
    //
    Arrays.sort(serTypes, new Comparator() {
      public int compare(Object o1, Object o2) {
        String n1 = ((JType) o1).getQualifiedSourceName();
        String n2 = ((JType) o2).getQualifiedSourceName();
        return n1.compareTo(n2);
      }
    });

    return serTypes;
  }

  private JType[] getSerializableTypes(GeneratorContext ctx) {
    if (serializableTypes == null) {
      serializableTypes = initializeSerializableTypes(ctx);
    }

    return serializableTypes;
  }

  private static final String SERIALIZE_IMPL_METHOD_SIGNATURE = "private native static void serializeImpl("
    + Shared.SERIALIZATION_STREAM_WRITER_INTERFACE_NAME
    + " streamWriter, Object instance) throws "
    + Shared.SERIALIZATION_EXCEPTION_CLASS_NAME;

  private void writeSerializeImplMethod(GeneratorContext ctx) {
    srcWriter.print(SERIALIZE_IMPL_METHOD_SIGNATURE);
    srcWriter.println(" /*-{");
    srcWriter.indent();
    srcWriter
      .println("var typeName = @com.google.gwt.core.client.GWT::getTypeName(Ljava/lang/Object;)(instance);");
    String serializerTypeName = serializationOracle
      .getTypeSerializerQualifiedName(getServiceInterface());
    srcWriter.println("var serializationSignature = @"
      + serializerTypeName
      + "::getSerializationSignatureImpl(Ljava/lang/String;)(typeName);");
    srcWriter.println("var serializedTypeName = typeName;");
    
    srcWriter.println("if (serializationSignature != null) {");
    srcWriter.indentln("serializedTypeName += \"/\" + serializationSignature;");
    srcWriter.println("}");
    
    srcWriter.println("switch (serializedTypeName) {");
    srcWriter.indent();
    JType[] types = getSerializableTypes(ctx);
    for (int index = 0; index < types.length; ++index) {
      JType type = types[index];
      
      // If this type is abstract it will not be serialized into the stream
      //
      if (isAbstractType(type)) {
        continue;
      }
      
      srcWriter.print("case \"");

      if (shouldEnforceTypeVersioning()) {
        srcWriter.print(serializationOracle
          .encodeSerializedInstanceReference(type));
      } else {
        srcWriter.print(serializationOracle.getSerializedTypeName(type));
      }

      srcWriter.println("\":");
      srcWriter.indent();

      String serializerName = serializationOracle.getFieldSerializerName(type);
      assert (serializerName != null);

      writeJSNISerializationStatement(type, serializerName);

      srcWriter.println("break;");
      srcWriter.outdent();
    }

    srcWriter.println("default:");
    srcWriter.indentln("@" + serializerTypeName
      + "::raiseSerializationException(Ljava/lang/String;)(typeName);");
    srcWriter.indentln("break;");

    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.outdent();
    srcWriter.println("}-*/;");
    srcWriter.println();
  }

  private static final String SERIALIZE_METHOD_SIGNATURE = "public void serialize("
    + Shared.SERIALIZATION_STREAM_WRITER_INTERFACE_NAME
    + " streamWriter, Object instance) throws "
    + Shared.SERIALIZATION_EXCEPTION_CLASS_NAME;

  private void writeSerializeMethod() {
    srcWriter.println(SERIALIZE_METHOD_SIGNATURE + " {");
    srcWriter.indent();
    srcWriter.print(Shared.SERIALIZATION_STREAM_OBJECT_ENCODER_INTERFACE_NAME);
    srcWriter.println(" encoder = streamWriter.getObjectEncoder();");
    srcWriter.println("if (encoder.startInstance(streamWriter, instance)) {");
    srcWriter.indentln("serializeImpl(streamWriter, instance);");
    srcWriter.println("}");
    srcWriter.println("encoder.endInstance(streamWriter, instance);");
    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.println();
  }

  /**
   * We create two versions of the getSerializationSignature methods. One is
   * static and it does the real work. The other is an instance method that
   * delegates to the static version.
   * 
   * @param ctx
   * @throws ClassNotFoundException
   */
  private void writeGetSerializationSignatureMethod(GeneratorContext ctx)
      throws ClassNotFoundException {
    srcWriter
      .println("public String getSerializationSignature(String serializedTypeName) {");
    srcWriter
    .indentln("return getSerializationSignatureImpl(serializedTypeName);");
    srcWriter.println("}");
  }

  private void writeGetSerializationSignatureMethodImpl(GeneratorContext ctx) {
    srcWriter
      .println("public static native String getSerializationSignatureImpl(String serializedTypeName) /*-{");
    if (!shouldEnforceTypeVersioning()) {
      srcWriter.indentln("return null;");
    } else {
      srcWriter.indent();
      srcWriter.println("switch (serializedTypeName) {");
      srcWriter.indent();
      JType[] types = getSerializableTypes(ctx);
      for (int index = 0; index < types.length; ++index) {
        JType type = types[index];
  
        srcWriter.println("case \""
          + serializationOracle.getSerializedTypeName(type) + "\":");
        srcWriter.indent();
  
        srcWriter.println("return \""
          + serializationOracle.getSerializationSignature(type) + "\";");
        srcWriter.outdent();
      }
  
      srcWriter.println("default:");
      String serializerTypeName = serializationOracle
        .getTypeSerializerQualifiedName(getServiceInterface());
      srcWriter
        .indentln("@"
          + serializerTypeName
          + "::raiseSerializationException(Ljava/lang/String;)(serializedTypeName);");
      srcWriter.indentln("break;");
  
      srcWriter.outdent();
      srcWriter.println("}");
      srcWriter.outdent();
    }
    
    srcWriter.println("}-*/;");
    srcWriter.println();

  }

  private static final String DESERIALIZE_IMPL_METHOD_SIGNATURE = "private native static Object deserializeImpl("
    + Shared.SERIALIZATION_STREAM_READER_INTERFACE_NAME
    + " streamReader, "
    + Shared.SERIALIZATION_STREAM_OBJECT_DECODER_INTERFACE_NAME
    + " decoder, "
    + "String typeName) throws " + Shared.SERIALIZATION_EXCEPTION_CLASS_NAME;

  private void writeDeserializeImplMethod(GeneratorContext ctx) {
    srcWriter.print(DESERIALIZE_IMPL_METHOD_SIGNATURE);
    srcWriter.println(" /*-{");
    srcWriter.indent();
    srcWriter.println("switch (typeName) {");
    srcWriter.indent();
    JType[] types = getSerializableTypes(ctx);

    for (int index = 0; index < types.length; ++index) {
      JType type = types[index];
      
      // If this type is abstract it will not be serialized into the stream
      //
      if (isAbstractType(type)) {
        continue;
      }

      srcWriter.print("case \"");

      if (shouldEnforceTypeVersioning()) {
        srcWriter.print(serializationOracle
          .encodeSerializedInstanceReference(type));
      } else {
        srcWriter.print(serializationOracle.getSerializedTypeName(type));
      }

      srcWriter.println("\":");
      srcWriter.indent();

      String serializerName = serializationOracle.getFieldSerializerName(type);
      assert (serializerName != null);
      srcWriter.print("var instance = @");

      writeJSNITypeInstantiationStatement(type);
      srcWriter.println("decoder.@"
        + Shared.SERIALIZATION_STREAM_OBJECT_DECODER_INTERFACE_NAME
        + "::rememberDecodedObject(Ljava/lang/Object;)(instance);");
      writeJSNIDeserializationStatement(type, serializerName);
    }

    srcWriter.println("default:");
    String serializerTypeName = serializationOracle
      .getTypeSerializerQualifiedName(getServiceInterface());
    srcWriter.indentln("@" + serializerTypeName
      + "::raiseSerializationException(Ljava/lang/String;)(typeName);");
    srcWriter.indentln("break;");

    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.outdent();
    srcWriter.println("}-*/;");
    srcWriter.println();
  }

  /**
   * Write a JSNI statement that will deserializer the requested type.
   * 
   * @param type
   * @param serializerName
   */
  private void writeJSNIDeserializationStatement(JType type,
      String serializerName) {
    String JSNISignature = normalizeJSNIInstanceSerializationMethodSignature(type);

    srcWriter.print("@" + serializerName);
    srcWriter.println("::deserialize(L"
      + Shared.SERIALIZATION_STREAM_READER_INTERFACE_NAME.replace('.', '/')
      + ";" + JSNISignature + ")(streamReader, instance);");
    srcWriter.println("return instance;");
    srcWriter.outdent();
  }

  /**
   * Write a JSNI statement that will serialize the requested type.
   * 
   * @param type
   * @param serializerName
   */
  private void writeJSNISerializationStatement(JType type, String serializerName) {
    String JSNISignature = normalizeJSNIInstanceSerializationMethodSignature(type);

    srcWriter.print("@" + serializerName);
    srcWriter.println("::serialize(L"
      + Shared.SERIALIZATION_STREAM_WRITER_INTERFACE_NAME.replace('.', '/')
      + ";" + JSNISignature + ")(streamWriter, instance);");
  }

  /**
   * Given a type determine what JSNI signature to use in the serialize or
   * deserialize method of a custom serializer.
   * 
   * @param type
   * @return
   */
  private String normalizeJSNIInstanceSerializationMethodSignature(JType type) {
    String JSNISignature;
    JArrayType arrayType = type.isArray();

    if (arrayType != null) {
      JType componentType = arrayType.getComponentType();
      JPrimitiveType primitiveType = componentType.isPrimitive();
      if (primitiveType != null) {
        JSNISignature = "[" + primitiveType.getJNISignature();
      } else {
        JSNISignature = "[" + "Ljava/lang/Object;";
      }
    } else {
      JSNISignature = type.getJNISignature();
    }

    return JSNISignature;
  }

  /**
   * Write a JSNI statement that will instantiate the requested type.
   * 
   * @param type
   */
  private void writeJSNITypeInstantiationStatement(JType type) {
    JMethod instantiationMethod = serializationOracle
      .getCustomFieldSerializerInstantiateMethodForType(type);
    if (instantiationMethod != null) {
      srcWriter.print(instantiationMethod.getEnclosingType()
        .getQualifiedSourceName());
      srcWriter.print("::");
      srcWriter.print(instantiationMethod.getName());
    } else {
      String serializerTypeName = serializationOracle
        .getTypeSerializerQualifiedName(getServiceInterface());
      srcWriter.print(serializerTypeName);
      srcWriter.print("::");
      srcWriter.print(getInstantiationMethodName(type));
    }

    srcWriter.println("(L"
      + Shared.SERIALIZATION_STREAM_READER_INTERFACE_NAME.replace('.', '/')
      + ";)(streamReader);");
  }

  private static final String DESERIALIZE_METHOD_SIGNATURE = "public Object deserialize("
    + Shared.SERIALIZATION_STREAM_READER_INTERFACE_NAME
    + "  streamReader)  throws " + Shared.SERIALIZATION_EXCEPTION_CLASS_NAME;

  private void writeDeserializeMethod(GeneratorContext ctx) {
    srcWriter.println(DESERIALIZE_METHOD_SIGNATURE + " {");
    srcWriter.indent();
    srcWriter.print(Shared.SERIALIZATION_STREAM_OBJECT_DECODER_INTERFACE_NAME);
    srcWriter.println(" decoder = streamReader.getObjectDecoder();");
    srcWriter.println("Object instance = decoder.prevInstance(streamReader);");
    srcWriter.println("if (instance != null) {");
    srcWriter.indentln("return instance;");
    srcWriter.println("}");
    srcWriter.println();
    srcWriter.println("String typeName = decoder.nextInstance(streamReader);");

    srcWriter.println("if (typeName == null) {");
    srcWriter.indentln("return null;");
    srcWriter.println("}");
    srcWriter.println();
    srcWriter
      .println("instance = deserializeImpl(streamReader, decoder, typeName);");
    srcWriter.println("return instance;");
    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.println();
  }

  private void writeInstantiationMethods(GeneratorContext context) {
    JType[] types = getSerializableTypes(context);
    for (int typeIndex = 0; typeIndex < types.length; ++typeIndex) {
      JType type = types[typeIndex];
      assert (serializationOracle.isSerializable(type));

      // If this type is abstract it will not be serialized into the stream
      //
      if (isAbstractType(type)) {
        continue;
      }

      JMethod instantiate = serializationOracle
        .getCustomFieldSerializerInstantiateMethodForType(type);
      
      if (instantiate != null) {
        // There is a custom serializer that implements an instantiation method
        // so we do not need one here.
        //
        continue;
      }
      
      srcWriter.print("private static ");
      srcWriter.print(type.getQualifiedSourceName());
      srcWriter.print(" ");
      srcWriter.print(getInstantiationMethodName(type));
      srcWriter.println("(" + Shared.SERIALIZATION_STREAM_READER_INTERFACE_NAME
        + " streamReader) throws " + Shared.SERIALIZATION_EXCEPTION_CLASS_NAME
        + " {");
      srcWriter.indent();

      JArrayType array = type.isArray();
      if (array != null) {
        writeArrayInstantiationMethod(array);
      } else {
        srcWriter.print("return new ");
        srcWriter.print(type.getQualifiedSourceName());
        srcWriter.println("();");
      }

      srcWriter.outdent();
      srcWriter.println("}");

      srcWriter.println();
    }
  }

  private boolean isAbstractType(JType type) {
    JClassType classType = type.isClassOrInterface(); 
    if (classType != null) {
      if (classType.isAbstract()) {
        return true;
      }
    }
    
    // Primitives, arrays, and non-abstract classes fall-through to here.
    //
    return false;
  }

  private String buildArrayInstantiationExpression(JArrayType array) {
    String expression = "[rank]";
    JType componentType = array.getComponentType();
    while (true) {
      array = componentType.isArray();
      if (array == null) {
        break;
      }

      expression += "[0]";

      componentType = array.getComponentType();
    }

    expression = componentType.getQualifiedSourceName() + expression;

    return expression;
  }

  private void writeArrayInstantiationMethod(JArrayType array) {
    srcWriter.println("int rank = streamReader.readInt();");
    srcWriter.println("return new " + buildArrayInstantiationExpression(array)
      + ";");
  }

  private String getInstantiationMethodName(JType type) {
    JArrayType arrayType = type.isArray();
    if (arrayType != null) {
      JType leafType = arrayType.getLeafType();
      return "create_" + leafType.getQualifiedSourceName().replace('.', '_')
        + "_Array_Rank_" + arrayType.getRank();
    }

    return "create_"
      + serializationOracle.getFieldSerializerName(type).replace('.', '_');
  }

  /*
   * Create all of the necessary field serializers.
   */
  private String createFieldSerializers(TreeLogger logger, GeneratorContext ctx)
      throws IOException {
    JType[] types = getSerializableTypes(ctx);
    int typeCount = types.length;
    String fieldSerializerNames = "";
    for (int typeIndex = 0; typeIndex < typeCount; ++typeIndex) {
      JType type = types[typeIndex];
      assert (type != null);

      String fieldSerializerName = createFieldSerializer(logger, ctx, type);
      if (fieldSerializerName != null) {
        fieldSerializerNames += fieldSerializerName + ";";
      }
    }

    return fieldSerializerNames;
  }

  private JType getStringType(GeneratorContext ctx) {
    if (stringType == null) {
      stringType = ctx.getTypeOracle().findType("java.lang.String");
      if (stringType == null) {
        throw new RuntimeException(
          "Unable to find a definition for java.lang.String");
      }

      return stringType;
    }

    return stringType;
  }

  /*
   * Create a field serializer for a type if it does not have a custom
   * serializer.
   */
  private String createFieldSerializer(TreeLogger logger, GeneratorContext ctx,
      JType type) throws IOException {
    assert (type != null);
    assert (serializationOracle.isSerializable(type));

    JParameterizedType parameterizedType = type.isParameterized();
    if (parameterizedType != null) {
      return createFieldSerializer(logger, ctx, parameterizedType.getRawType());
    }

    TypeOracle typeOracle = ctx.getTypeOracle();
    assert (typeOracle != null);

    JClassType customFieldSerializer = serializationOracle
      .hasCustomFieldSerializer(type);

    if (customFieldSerializer != null) {
      return customFieldSerializer.getQualifiedSourceName();
    }

    FieldSerializerCreator creator = new FieldSerializerCreator(
      serializationOracle, type);

    return creator.realize(logger, ctx);
  }

  private void setServiceInterface(JClassType serviceInterface) {
    assert (serviceInterface != null);

    svcInterface = serviceInterface;
  }

  private SourceWriter getSourceWriter(TreeLogger logger, GeneratorContext ctx)
      throws IOException {
    JClassType serviceIntf = getServiceInterface();
    JPackage serviceIntfPackage = serviceIntf.getPackage();
    String packageName = serviceIntfPackage != null ? serviceIntfPackage.getName() : "";
    String className = serializationOracle
      .getTypeSerializerSimpleName(getServiceInterface());
    PrintWriter printWriter = ctx.tryCreate(logger, packageName, className);
    if (printWriter == null) {
      return null;
    }

    ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
      packageName, className);

    composerFactory.addImplementedInterface(Serializer.class);

    return composerFactory.createSourceWriter(ctx, printWriter);
  }

  private JClassType getServiceInterface() {
    return svcInterface;
  }
}
